/*
 * Copyright (C) 2015 Miquel Sas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package traderlib.core.data.db;

import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import traderlib.core.data.db.sql.AddCheck;
import traderlib.core.data.db.sql.AddColumn;
import traderlib.core.data.db.sql.AddForeignKey;
import traderlib.core.data.db.sql.AddPrimaryKey;
import traderlib.core.data.db.sql.CreateIndex;
import traderlib.core.data.db.sql.CreateTable;
import traderlib.core.data.db.sql.Delete;
import traderlib.core.data.db.sql.DropColumn;
import traderlib.core.data.db.sql.DropConstraint;
import traderlib.core.data.db.sql.DropForeignKey;
import traderlib.core.data.db.sql.DropIndex;
import traderlib.core.data.db.sql.DropPrimaryKey;
import traderlib.core.data.db.sql.DropTable;
import traderlib.core.data.db.sql.Insert;
import traderlib.core.data.db.sql.Select;
import traderlib.core.data.db.sql.Update;
import traderlib.core.data.types.Value;

/**
 * A <code>DatabaseAdapter</code> represents the back-end system to which we connect through a JDBC driver. This class
 * addresses several problems faced when building systems that aim to be as much independent of the underlying database
 * as possible.
 * <p>
 * <b>Type mapping and table creation</b>. Although JDBC drivers may include helper classes or methods to map JDBC types
 * to the names used to create tables, this is always a particular and optional feature of the driver. Thus, a
 * parameterized type map to build <code>CREATE TABLE</code> statements is necessary for the system to address table
 * creation. Note that none of the database access specifications (JDBC, ODBC!) has a solution for this problem and
 * tables are supposed to exist or to be created by other means or hand written statements that are invalid applied to
 * other database systems.
 * <p>
 * <b>Relational queries</b>. Unfortunately, relations have to be expressed in a different way from one database to
 * another. Explicit and implicit relations should be supported for a system to be compatible. Explicit relations take
 * the form,
 * <p>
 * <code>
 * SELECT Detail.product_id, Product.description<br>
 * FROM Detail LEFT OUTER JOIN Product ON Detail.product_id = Product.id
 * </code>
 * <p>
 * while the same implicit relation would look like,
 * <p>
 * <code>
 * SELECT Detail.product_id, Product.description <br>
 * FROM Detail, Product<br>
 * WHERE Detail.product_id = Product.id(+)
 * </code>
 * <p>
 * <b>System functions.</b> System functions like the current date, time or timestamp unfortunatly have different names
 * for different databases. This class addresses this issue by mapping database function names for the current date,
 * time or timestamp.
 *
 * @author Miquel Sas
 */
public abstract class DatabaseAdapter {

	/**
	 * The JDBC driver class name.
	 */
	private String driverClassName;

	/**
	 * Default constructor.
	 */
	public DatabaseAdapter() {
		super();
	}

	/**
	 * Returns the CURRENT DATE function as a string.
	 *
	 * @return The CURRENT DATE function as a string.
	 */
	public abstract String getCurrentDate();

	/**
	 * Returns the CURRENT TIME function as a string.
	 *
	 * @return The CURRENT TIME function as a string.
	 */
	public abstract String getCurrentTime();

	/**
	 * Returns the CURRENT TIMESTAMP function as a string.
	 *
	 * @return The CURRENT TIMESTAMP function as a string.
	 */
	public abstract String getCurrentTimestamp();

	/**
	 * Gets the column definition to use in a <code>CREATE TABLE</code> statement, given a column.
	 *
	 * @return The column definition.
	 * @param column The column.
	 */
	public abstract String getColumnDefinition(Column column);

	/**
	 * Check if the underlying database accepts explicit relations.
	 *
	 * @return A boolean.
	 */
	public abstract boolean isExplicitRelation();

	/**
	 * Check if the underlying database accepts implicit relations.
	 *
	 * @return A boolean.
	 */
	public boolean isImplicitRelation() {
		return !isExplicitRelation();
	}

	/**
	 * Get the driver class name.
	 * <p>
	 * @return The driver class name.
	 */
	public String getDriverClassName() {
		return driverClassName;
	}

	/**
	 * Set the driver class name.
	 * <p>
	 * @param driverClassName The driver class name.
	 */
	protected void setDriverClassName(String driverClassName) {
		this.driverClassName = driverClassName;
	}

	/**
	 * Register the JDBC driver.
	 */
	public void registerDriver() {
		try {
			String driverName = getDriverClassName();
			Class<?> driverClass = Class.forName(driverName);
			Driver driver = (Driver) driverClass.newInstance();
			DriverManager.registerDriver(driver);
		} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | SQLException e) {
			e.printStackTrace(System.err);
		}
	}

	/**
	 * Returns the suffix part of a column definition, that is standard, with DEFAULT values and NOT NULL.
	 *
	 * @param column The argument column.
	 * @return The suffix part of the column definition.
	 */
	public String getColumnDefinitionSuffix(Column column) {
		StringBuilder b = new StringBuilder();
		if (column.isCurrentDateTimeOrTimestamp()) {
			b.append("DEFAULT ");
			if (column.isCurrentDate()) {
				b.append(getCurrentDate());
			}
			if (column.isCurrentTime()) {
				b.append(getCurrentTime());
			}
			if (column.isCurrentTimestamp()) {
				b.append(getCurrentTimestamp());
			}
		} else {
			if (column.getInitialValue() != null) {
				b.append(" DEFAULT ?");
			}
			if (!column.isNullable()) {
				if (column.getInitialValue() == null) {
					if (!column.isDate() && !column.isTime() && !column.isTimestamp()) {
						b.append(" DEFAULT ?");
					}
				}
				b.append(" NOT NULL");
			}
		}
		return b.toString();
	}

	/**
	 * Returns the column definition suffix value to set in the SQL parameter.
	 *
	 * @param column The column
	 * @return The value or null.
	 */
	public Value getColumnDefinitionSuffixValue(Column column) {
		if (!column.isCurrentDateTimeOrTimestamp()) {
			if (column.getInitialValue() != null) {
				return column.getInitialValue();
			}
			if (!column.isNullable()) {
				if (column.getInitialValue() == null) {
					if (!column.isDate() && !column.isTime() && !column.isTimestamp()) {
						return column.getBlankValue();
					}
				}
			}
		}
		return null;
	}

	/**
	 * Returns the add check statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the ADD CONSTRAINT ... CHECK statement.
	 *
	 * @return The delete command.
	 */
	public AddCheck getStatementAddCheck() {
		AddCheck addCheck = new AddCheck();
		addCheck.setDatabaseAdapter(this);
		return addCheck;
	}

	/**
	 * Returns the add column statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the ALTER TABLE <i>table</i> ADD <i>column definition</i> statement.
	 *
	 * @return The add column statement.
	 */
	public AddColumn getStatementAddColumn() {
		AddColumn addColumn = new AddColumn();
		addColumn.setDatabaseAdapter(this);
		return addColumn;
	}

	/**
	 * Returns the add foreign key statement. This method is aimed to be overwritten if the database adapter has a
	 * different syntax for the ALTER TABLE <i>table</i> ADD FOREIGN KEY <i>column definition</i> statement.
	 *
	 * @return The add foreign statement.
	 */
	public AddForeignKey getStatementAddForeignKey() {
		AddForeignKey addForeignKey = new AddForeignKey();
		addForeignKey.setDatabaseAdapter(this);
		return addForeignKey;
	}

	/**
	 * Returns the add primary key statement. This method is aimed to be overwritten if the database adapter has a
	 * different syntax for the ALTER TABLE ADD PRIMARY KEY statement.
	 *
	 * @return The add primary key statement.
	 */
	public AddPrimaryKey getStatementAddPrimaryKey() {
		AddPrimaryKey addPrimaryKey = new AddPrimaryKey();
		addPrimaryKey.setDatabaseAdapter(this);
		return addPrimaryKey;
	}

	/**
	 * Returns the create index statement. This method is aimed to be overwritten if the database adapter has a
	 * different syntax for the CREATE INDEX statement.
	 *
	 * @return The create table statement.
	 */
	public CreateIndex getStatementCreateIndex() {
		CreateIndex createIndex = new CreateIndex();
		createIndex.setDatabaseAdapter(this);
		return createIndex;
	}

	/**
	 * Returns the create table statement. This method is aimed to be overwritten if the database adapter has a
	 * different syntax for the CREATE TABLE statement.
	 *
	 * @return The create table statement.
	 */
	public CreateTable getStatementCreateTable() {
		CreateTable createTable = new CreateTable();
		createTable.setDatabaseAdapter(this);
		return createTable;
	}

	/**
	 * Returns the delete statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the DELETE statement.
	 *
	 * @return The delete statement.
	 */
	public Delete getStatementDelete() {
		Delete delete = new Delete();
		delete.setDatabaseAdapter(this);
		return delete;
	}

	/**
	 * Returns the drop column statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the ALTER TABLE <i>table</i> DROP COLUMN <i>column</i> statement.
	 *
	 * @return The drop column statement.
	 */
	public DropColumn getStatementDropColumn() {
		DropColumn dropColumn = new DropColumn();
		dropColumn.setDatabaseAdapter(this);
		return dropColumn;
	}

	/**
	 * Returns the drop constraint statement. This method is aimed to be overwritten if the database adapter has a
	 * different syntax for the ALTER TABLE <i>table</i> DROP CONSTRAINT <i>constraint</i> statement.
	 *
	 * @return The drop constraint statement.
	 */
	public DropConstraint getStatementDropConstraint() {
		DropConstraint dropConstraint = new DropConstraint();
		dropConstraint.setDatabaseAdapter(this);
		return dropConstraint;
	}

	/**
	 * Returns the drop constraint statement. This method is aimed to be overwritten if the database adapter has a
	 * different syntax for the ALTER TABLE <i>table</i> DROP FOREIGN KEY statement.
	 *
	 * @return The drop foreign key statement.
	 */
	public DropForeignKey getStatementDropForeignKey() {
		DropForeignKey dropForeignKey = new DropForeignKey();
		dropForeignKey.setDatabaseAdapter(this);
		return dropForeignKey;
	}

	/**
	 * Returns the drop index statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the DROP INDEX statement.
	 *
	 * @return The drop index statement.
	 */
	public DropIndex getStatementDropIndex() {
		DropIndex dropIndex = new DropIndex();
		dropIndex.setDatabaseAdapter(this);
		return dropIndex;
	}

	/**
	 * Returns the drop primary key statement. This method is aimed to be overwritten if the database adapter has a
	 * different syntax for the ALTER TABLE <i>table</i> DROP PRIMARY KEY statement.
	 *
	 * @return The drop primary key statement.
	 */
	public DropPrimaryKey getStatementDropPrimearyKey() {
		DropPrimaryKey dropPrimaryKey = new DropPrimaryKey();
		dropPrimaryKey.setDatabaseAdapter(this);
		return dropPrimaryKey;
	}

	/**
	 * Returns the drop table statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the DROP TABLE statement.
	 *
	 * @return The drop table statement.
	 */
	public DropTable getStatementDropTable() {
		DropTable dropTable = new DropTable();
		dropTable.setDatabaseAdapter(this);
		return dropTable;
	}

	/**
	 * Returns the insert statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the INSERT INTO statement.
	 *
	 * @return The insert statement.
	 */
	public Insert getStatementInsert() {
		Insert insert = new Insert();
		insert.setDatabaseAdapter(this);
		return insert;
	}

	/**
	 * Returns the select query. This method is aimed to be overwritten if the database adapter has a different syntax
	 * for the SELECT query.
	 *
	 * @return The select query.
	 */
	public Select getQuerySelect() {
		Select select = new Select();
		select.setDatabaseAdapter(this);
		return select;
	}

	/**
	 * Returns the update statement. This method is aimed to be overwritten if the database adapter has a different
	 * syntax for the UPDATE statement.
	 *
	 * @return The update statement.
	 */
	public Update getStatementUpdate() {
		Update update = new Update();
		update.setDatabaseAdapter(this);
		return update;
	}
}
